/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Nvigorate.Common.Reflection;
using Nvigorate.Extensions;
using Nvigorate.Relational.Mapping.Interface;

namespace Nvigorate.Relational.Mapping.Fluent
{
    public interface IFluentMap
    {
        IBindingMap GetMap();
    }

    public class FluentMap<TClass> : IFluentMap
    {
        #region Fields

        private List<IFluentRelationship> Relationships { get; set; }
        private Dictionary<string, FluentSourceBase<TClass>> Sources { get; set; }
        private string TargetName { get { return typeof(TClass).GetCleanAssemblyQualifiedTypeName(); } set { } }
        private Type TargetType { get { return typeof(TClass); } set { } }
        private IDiscriminate _discriminator;

        #endregion

        #region Properties

        public bool CanDelete { get; set; }
        public bool CanInsert { get; set; }
        public bool CanSelect { get; set; }
        public bool CanUpdate { get; set; }
        public string RepositoryName { get; set; }
        public FluentSourceBase<TClass> this[string tableName]
        {
            get { return Sources.ContainsKey(tableName) ? Sources[tableName] : null; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a BindingMap object from the FluentMap
        /// </summary>
        /// <returns>A BindingMap which gets used during persistence</returns>
        public IBindingMap GetMap()
        {
            return new BindingMap()
                       {
                           CanDelete = CanDelete,
                           CanInsert = CanInsert,
                           CanSelect = CanSelect,
                           CanUpdate = CanUpdate,
                           Discriminator = _discriminator,
                           RepositoryName = RepositoryName,
                           TargetType = TargetType,
                           Relationships = Relationships.Select(r => r.GetRelationship()).ToList(),
                           Sources = Sources.Select(p => p.Value.GetSourceMap()).ToDictionary(s => s.SourceName, s => s)
                       };
        }

        public FluentSource<TClass> InheritSource<TFluentMap, TParent>(string sourceName) where TFluentMap : FluentMap<TParent>, new()
        {
            var parentMap = new TFluentMap();
            RepositoryName = parentMap.RepositoryName;
            _discriminator = parentMap._discriminator;

            var parentSource = parentMap[sourceName];
            var newSource = new FluentSource<TClass>(sourceName);
            newSource.Import(parentSource);
            Sources.Add(sourceName, newSource);
            return newSource;
        }

        public void InheritMap<TFluentMap, TParent>() where TFluentMap : FluentMap<TParent>, new()
        {
            var parentMap = new TFluentMap();
            RepositoryName = parentMap.RepositoryName;
            _discriminator = parentMap._discriminator;

            parentMap.Sources.ForEach(s =>
                                          {
                                              var newSource = new FluentSource<TClass>(s.Key);
                                              newSource.Import(s.Value);
                                              Sources.Add(newSource.Name, newSource);
                                          });
        }

        /// <summary>
        /// Creates a mapping from the type to a database table.
        /// The assumption is that the table name matches the Type name
        /// </summary>
        /// <returns>A FluentSource which is used to define how the type maps to a table</returns>
        public FluentSource<TClass> MapTable()
        {
            var tableName = TargetType.Name;
            var source = new FluentSource<TClass>(tableName);
            Sources.Add(tableName, source);
            return source;
        }

        public ColumnPresenceDiscriminator DiscriminateOnColumnPresence()
        {
            _discriminator = new ColumnPresenceDiscriminator(typeof (TClass));
            return _discriminator as ColumnPresenceDiscriminator;
        }

        public ColumnValueDiscriminator DiscriminateOnColumnValue()
        {
            _discriminator = new ColumnValueDiscriminator(typeof(TClass));
            return _discriminator as ColumnValueDiscriminator;
        }

        /// <summary>
        /// Creates a mapping from the type to a database table.
        /// </summary>
        /// <param name="tableName">The name of the table to map to the type</param>
        /// <returns>A FluentSource which is used to define how the type maps to a table</returns>
        public FluentSource<TClass> MapTable(string tableName)
        {
            var source = new FluentSource<TClass>(tableName);
            Sources.Add(tableName, source);
            return source;
        }

        /// <summary>
        /// Creates a mapping from the type to a database table.
        /// </summary>
        /// <param name="tableName">The name of the table to map to the type</param>
        /// <returns>A FluentSource which is used to define how the type maps to a table</returns>
        public FluentSource<TClass, TSchema> MapTable<TSchema>()
        {
            var source = new FluentSource<TClass, TSchema>();
            Sources.Add(source.TableName, source);
            return source;
        }

        /// <summary>
        /// Defines a one to one relationship between types
        /// </summary>
        /// <typeparam name="TChildProperty"></typeparam>
        /// <param name="childProperty"></param>
        /// <returns></returns>
        public FluentOneToMany<TClass, TChildProperty> MapChild<TChildProperty>(Expression<Func<TClass, TChildProperty>> childProperty)
        {
            var expression = childProperty.Body as MemberExpression;
            var relationship = new FluentOneToMany<TClass, TChildProperty>(expression.Member.Name, Relationship.Parent);
            Relationships.Add(relationship);
            return relationship;
        }

        /// <summary>
        /// Defines a one to many relationship between types
        /// </summary>
        /// <typeparam name="TChildrenProperty"></typeparam>
        /// <param name="childProperty"></param>
        /// <returns></returns>
        public FluentOneToMany<TClass, TChildrenProperty> MapChildren<TChildrenProperty>(Expression<Func<TClass, IEnumerable<TChildrenProperty>>> childProperty)
        {
            var expression = childProperty.Body as MemberExpression;
            var relationship = new FluentOneToMany<TClass, TChildrenProperty>(expression.Member.Name, Relationship.Parent);
            Relationships.Add(relationship);
            return relationship;
        }

        /// <summary>
        /// Defines a many to one (parent backreference) relationship between types
        /// </summary>
        /// <typeparam name="TParentProperty"></typeparam>
        /// <param name="parentProperty"></param>
        /// <returns></returns>
        public FluentOneToMany<TClass, TParentProperty> MapParent<TParentProperty>(Expression<Func<TClass, TParentProperty>> parentProperty)
        {
            var expression = parentProperty.Body as MemberExpression;
            var parent = expression.Member.Name;
            var relationship = new FluentOneToMany<TClass, TParentProperty>(parent, Relationship.Child);
            Relationships.Add(relationship);

            this.Sources.Values
                .SelectMany(s => s.Assignments)
                .Where(a => a.targetField.Split('.')[0] == parent)
                .ForEach(a => a.foreignKey = true);

            return relationship;
        }

        /// <summary>
        /// Defines a many to many relationship between types
        /// </summary>
        /// <typeparam name="TRelativeProperty"></typeparam>
        /// <param name="peerProperty"></param>
        /// <param name="relationTableName"></param>
        /// <returns></returns>
        public FluentManyToMany<TClass, TRelativeProperty> MapToMany<TRelativeProperty>(Expression<Func<TClass, IEnumerable<TRelativeProperty>>> peerProperty, string relationTableName)
        {
            var expression = peerProperty.Body as MemberExpression;
            var relationship = new FluentManyToMany<TClass, TRelativeProperty>(expression.Member.Name, Relationship.Peer, relationTableName);
            Relationships.Add(relationship);
            return relationship;
        }

        /// <summary>
        /// Defines a many to many relationship between types
        /// </summary>
        /// <typeparam name="TRelativeProperty"></typeparam>
        /// <param name="peerProperty"></param>
        /// <param name="relationTableName"></param>
        /// <returns></returns>
        public FluentManyToMany<TClass, TRelativeProperty, TContainerSchema> MapToMany<TRelativeProperty, TContainerSchema>(Expression<Func<TClass, IEnumerable<TRelativeProperty>>> peerProperty)
        {
            var expression = peerProperty.Body as MemberExpression;
            var relationship = new FluentManyToMany<TClass, TRelativeProperty, TContainerSchema>(expression.Member.Name, Relationship.Peer);
            Relationships.Add(relationship);
            return relationship;
        }

        #endregion

        public FluentMap()
        {
            CanInsert = true;
            CanDelete = true;
            CanSelect = true;
            CanUpdate = true;
            Relationships = new List<IFluentRelationship>();
            Sources = new Dictionary<string, FluentSourceBase<TClass>>();
        }
    }
}